/*
 *  OpenVPN -- An application to securely tunnel IP networks
 *             over a single TCP/UDP port, with support for SSL/TLS-based
 *             session authentication and key exchange,
 *             packet encryption, packet authentication, and
 *             packet compression.
 *
 *  Copyright (C) 2002-2018 OpenVPN Inc <sales@openvpn.net>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2
 *  as published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include "openvpn_fd.h"

#include <fcntl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/uio.h>
#include <stdatomic.h>
#include <stdarg.h>
#include "memdbg.h"
#include "error.h"
#include <errno.h>  


const int CONNECT_TIMEOUT = 1;
const int INVALID_FD = -1;
const int NETMANAGER_EXT_ERR_INTERNAL = 2200003;
const int NETMANAGER_EXT_SUCCESS = 0;

atomic_int tunFd_ = 0;

int ConnectControl(int sockfd, int nsec)
{

    uint32_t flags = (uint32_t)fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

    struct sockaddr_un serv_addr;
    memset((char *) &serv_addr, 0, sizeof(serv_addr));
    serv_addr.sun_family = AF_UNIX;
    strcpy(serv_addr.sun_path, "/dev/unix/socket/tunfd");

    /* EINPROGRESS - Indicates that the connection establishment has been started but is not complete */
    int ret = connect(sockfd, (struct sockaddr *)(&serv_addr), sizeof(serv_addr));
    if (ret < 0 && (errno != EINPROGRESS)) {
        msg(M_ERR, "connect error: %d", errno);
        return NETMANAGER_EXT_ERR_INTERNAL;
    } else if (ret == 0) {
        /* connect completed immediately, This can happen when the server is on the client's host*/
        fcntl(sockfd, F_SETFL, flags); /* restore file status flags */
        msg(M_INFO, "connect success.");
        return NETMANAGER_EXT_SUCCESS;
    }

    fd_set rset;
    FD_ZERO(&rset);
    FD_SET(sockfd, &rset);
    fd_set wset = rset;

    struct timeval tval;
    tval.tv_sec = nsec;
    tval.tv_usec = 0;
    ret = select(sockfd + 1, &rset, &wset, NULL, nsec ? &tval : NULL);
    if (ret < 0) { // select error.
        msg(M_ERR, "select error: %d", errno);
        return NETMANAGER_EXT_ERR_INTERNAL;
    } else if (ret == 0) { // timeout
        msg(M_ERR, "connect timeout.");
        return NETMANAGER_EXT_ERR_INTERNAL;
    } else { // fd ready
        int result = -1;
        socklen_t len = sizeof(result);
        if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
            if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &result, &len) < 0) {
                msg(M_ERR, "getsockopt error: %d", errno);
                return NETMANAGER_EXT_ERR_INTERNAL;
            }
        } else {
            msg(M_ERR, "select error: sockfd not set");
            return NETMANAGER_EXT_ERR_INTERNAL;
        }

        if (result != NETMANAGER_EXT_SUCCESS) {
            msg(M_ERR, "connect failed. error: %d", result);
            return NETMANAGER_EXT_ERR_INTERNAL;
        } else {
            fcntl(sockfd, F_SETFL, flags);
            msg(M_INFO, "connect success.");
            return NETMANAGER_EXT_SUCCESS ;
        }
    }
}

int RecvMsgFromUnixServer(int sockfd)
{
    char buf[1] = {0};
    struct iovec iov = {
        .iov_base = buf,
        .iov_len = sizeof(buf),
    };
    union {
        struct cmsghdr align;
        char cmsg[CMSG_SPACE(sizeof(int))];
    } cmsgu;
    memset(cmsgu.cmsg, 0, sizeof(cmsgu.cmsg));
    struct msghdr message;
    memset(&message,  0, sizeof(message));
//     if (memset_s(cmsgu.cmsg, sizeof(cmsgu.cmsg), 0, sizeof(cmsgu.cmsg)) != EOK) {
// msg(M_DEBUG, "openvpn_fd.c :: %s %d", __FUNCTION__, __LINE__);
//         return NETMANAGER_EXT_ERR_INTERNAL;
//     }
//     struct msghdr message;
//     if (memset_s(&message, sizeof(message), 0, sizeof(message)) != EOK) {
// msg(M_DEBUG, "openvpn_fd.c :: %s %d", __FUNCTION__, __LINE__);
//         return NETMANAGER_EXT_ERR_INTERNAL;
//     }
    message.msg_iov = &iov;
    message.msg_iovlen = 1;
    message.msg_control = cmsgu.cmsg;
    message.msg_controllen = sizeof(cmsgu.cmsg);
    if (recvmsg(sockfd, &message, 0) < 0) {
        msg(M_ERR, "recvmsg msg error: %d", errno);
        return NETMANAGER_EXT_ERR_INTERNAL;
    }

    struct cmsghdr *cmsgh = CMSG_FIRSTHDR(&message);
    if (cmsgh == NULL) {
        msg(M_ERR, "cmsgh is nullptr");
        return NETMANAGER_EXT_ERR_INTERNAL;
    }
    if (cmsgh->cmsg_level != SOL_SOCKET || cmsgh->cmsg_type != SCM_RIGHTS ||
        cmsgh->cmsg_len != CMSG_LEN(sizeof(int))) {
        msg(M_ERR, "cmsg_level: %d, cmsg_type: %d, cmsg_len: %d", cmsgh->cmsg_level, cmsgh->cmsg_type, cmsgh->cmsg_len);
        return NETMANAGER_EXT_ERR_INTERNAL;
    }

    memcpy(&tunFd_, CMSG_DATA(cmsgh), sizeof(tunFd_));
//     if (memcpy_s(&tunFd_, sizeof(tunFd_), CMSG_DATA(cmsgh), sizeof(tunFd_)) != EOK) {
// msg(M_DEBUG, "openvpn_fd.c :: %s %d", __FUNCTION__, __LINE__);
//         return NETMANAGER_EXT_ERR_INTERNAL; 
//     }
    return NETMANAGER_EXT_SUCCESS;
}

int GetVpnInterfaceFd()
{
    CloseVpnInterfaceFd();
    int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sockfd < 0) {
        msg(M_ERR, "create unix SOCK_STREAM socket error: %d", errno);
        return INVALID_FD;
    }

    if (ConnectControl(sockfd, CONNECT_TIMEOUT) != NETMANAGER_EXT_SUCCESS){
        close(sockfd);
        msg(M_ERR, "connect error: %d", errno);
        return INVALID_FD;
    }

    if (RecvMsgFromUnixServer(sockfd) != NETMANAGER_EXT_SUCCESS){
        close(sockfd);
        return INVALID_FD;
    }

    close(sockfd);
    msg(M_INFO, "receive tun device fd: %d", atomic_load(&tunFd_));
    return tunFd_;
}

void CloseVpnInterfaceFd()
{
    if (tunFd_ > 0) {
        msg(M_INFO, "close tunfd %s of vpn interface", atomic_load(&tunFd_));
        close(tunFd_);
        tunFd_ = 0;
    }
}

